using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Svelto.DataStructures;

namespace Svelto.ECS
{
    /// <summary>
    ///     This mechanism is not for thread-safety but to be sure that all the permutations of group tags always
    ///     point to the same group ID.
    ///     A group compound can generate several permutation of tags, so that the order of the tag doesn't matter,
    ///     but for this to work, each permutation must be identified by the same ID (generated by the unique combination)
    ///     each permutation of the same groups of tag is actually a different class with a different static constructor and we
    ///     don't want to call it more than once for the same set of tags
    ///     it's thread local because since it's not linked to a specific group, it must work synchronously
    /// </summary>
    static class GroupCompoundInitializer
    {
        internal static readonly ThreadLocal<bool> skipStaticCompoundConstructorsWith4Tags = new ThreadLocal<bool>();
        internal static readonly ThreadLocal<bool> skipStaticCompoundConstructorsWith3Tags = new ThreadLocal<bool>();
        internal static readonly ThreadLocal<bool> skipStaticCompoundConstructorsWith2Tags = new ThreadLocal<bool>();
    }

    interface ITouchedByReflection { }

    public abstract class GroupCompound<G1, G2, G3, G4>: ITouchedByReflection
            where G1 : GroupTag<G1>
            where G2 : GroupTag<G2>
            where G3 : GroupTag<G3>
            where G4 : GroupTag<G4>
    {
        static GroupCompound()
        {
            /// c# Static constructors are guaranteed to be thread safe and not called more than once
            if (Interlocked.CompareExchange(ref isInitialised, 1, 0) != 0)
                throw new Exception("GroupCompound static constructor called twice - impossible");
            
            if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value == false)
            {
                var group = new ExclusiveGroup(GroupTag<G1>.bitmask | GroupTag<G2>.bitmask | GroupTag<G3>.bitmask | GroupTag<G4>.bitmask);

                _Groups = new FasterList<ExclusiveGroupStruct>(1);
                _Groups.Add(group);
#if DEBUG
                var name =
                        $"Compound: {typeof(G1).Name}-{typeof(G2).Name}-{typeof(G3).Name}-{typeof(G4).Name} ID {(uint)group.id}";
                GroupNamesMap.idToName[group] = name;
#endif
                //The hashname is independent from the actual group ID. this is fundamental because it is want
                //guarantees the hash to be the same across different machines
                GroupHashMap.RegisterGroup(group, typeof(GroupCompound<G1, G2, G3, G4>).FullName);

                //ToArrayFast is theoretically not correct, but since multiple 0s are ignored and we don't care if we 
                //add one, we avoid an allocation
                var exclusiveGroupStructs = _Groups.ToArrayFast(out var count);
                _GroupsHashSet = new HashSet<ExclusiveGroupStruct>(count);
                for (var index = 0; index < count; ++index)
                {
                    var exclusiveGroupStruct = exclusiveGroupStructs[index];
                    _GroupsHashSet.Add(exclusiveGroupStruct);
                }

                GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value = true;

                //all the permutations must share the same group and group hashset. Warm them up, avoid call the 
                //constructors again, set the desired value
                GroupCompound<G1, G2, G4, G3>._Groups = _Groups;
                GroupCompound<G1, G3, G2, G4>._Groups = _Groups;
                GroupCompound<G1, G3, G4, G2>._Groups = _Groups;
                GroupCompound<G1, G4, G2, G3>._Groups = _Groups;
                GroupCompound<G2, G1, G3, G4>._Groups = _Groups;
                GroupCompound<G2, G3, G4, G1>._Groups = _Groups;
                GroupCompound<G3, G1, G2, G4>._Groups = _Groups;
                GroupCompound<G4, G1, G2, G3>._Groups = _Groups;
                GroupCompound<G1, G4, G3, G2>._Groups = _Groups;
                GroupCompound<G2, G1, G4, G3>._Groups = _Groups;
                GroupCompound<G2, G4, G3, G1>._Groups = _Groups;
                GroupCompound<G3, G1, G4, G2>._Groups = _Groups;
                GroupCompound<G4, G1, G3, G2>._Groups = _Groups;
                GroupCompound<G2, G3, G1, G4>._Groups = _Groups;
                GroupCompound<G3, G4, G1, G2>._Groups = _Groups;
                GroupCompound<G2, G4, G1, G3>._Groups = _Groups;
                GroupCompound<G3, G2, G1, G4>._Groups = _Groups;
                GroupCompound<G3, G2, G4, G1>._Groups = _Groups;
                GroupCompound<G3, G4, G2, G1>._Groups = _Groups;
                GroupCompound<G4, G2, G1, G3>._Groups = _Groups;
                GroupCompound<G4, G2, G3, G1>._Groups = _Groups;
                GroupCompound<G4, G3, G1, G2>._Groups = _Groups;
                GroupCompound<G4, G3, G2, G1>._Groups = _Groups;

                //all the constructor have been called now
                GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value = false;

                GroupCompound<G1, G2, G4, G3>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G1, G3, G2, G4>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G1, G3, G4, G2>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G1, G4, G2, G3>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G2, G1, G3, G4>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G2, G3, G4, G1>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G3, G1, G2, G4>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G4, G1, G2, G3>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G1, G4, G3, G2>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G2, G1, G4, G3>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G2, G4, G3, G1>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G3, G1, G4, G2>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G4, G1, G3, G2>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G2, G3, G1, G4>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G3, G4, G1, G2>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G2, G4, G1, G3>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G3, G2, G1, G4>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G3, G2, G4, G1>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G3, G4, G2, G1>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G4, G2, G1, G3>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G4, G2, G3, G1>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G4, G3, G1, G2>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G4, G3, G2, G1>._GroupsHashSet = _GroupsHashSet;
                
                GroupCompound<G1, G2, G3>.Add(group);
                GroupCompound<G1, G2, G4>.Add(group);
                GroupCompound<G1, G3, G4>.Add(group);
                GroupCompound<G2, G3, G4>.Add(group);

                GroupCompound<G1, G2>.Add(group); //<G1/G2> and <G2/G1> must share the same array
                GroupCompound<G1, G3>.Add(group);
                GroupCompound<G1, G4>.Add(group);
                GroupCompound<G2, G3>.Add(group);
                GroupCompound<G2, G4>.Add(group);
                GroupCompound<G3, G4>.Add(group);

                //This is done here to be sure that the group is added once per group tag
                //(if done inside the previous group compound it would be added multiple times)
                GroupTag<G1>.Add(group);
                GroupTag<G2>.Add(group);
                GroupTag<G3>.Add(group);
                GroupTag<G4>.Add(group);
            }
        }

        public static FasterReadOnlyList<ExclusiveGroupStruct> Groups
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => new(_Groups);
        }

        public static ExclusiveBuildGroup BuildGroup
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => new(_Groups[0], 1);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool Includes(ExclusiveGroupStruct group)
        {
            DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed");

            return _GroupsHashSet.Contains(group);
        }

        internal static void Add(ExclusiveGroupStruct group)
        {
#if DEBUG && !PROFILE_SVELTO
            for (var i = 0; i < _Groups.count; ++i)
                if (_Groups[i] == group)
                    throw new System.Exception("this test must be transformed in unit test");
#endif

            _Groups.Add(group);
            _GroupsHashSet.Add(group);
        }

        static readonly FasterList<ExclusiveGroupStruct> _Groups;
        static readonly HashSet<ExclusiveGroupStruct> _GroupsHashSet;

        //we are changing this with Interlocked, so it cannot be readonly
        static int isInitialised;
    }

    public abstract class GroupCompound<G1, G2, G3>: ITouchedByReflection
            where G1 : GroupTag<G1>
            where G2 : GroupTag<G2>
            where G3 : GroupTag<G3>
    {
        static GroupCompound()
        {
            /// c# Static constructors are guaranteed to be thread safe and not called more than once
            if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0)
                throw new Exception("GroupCompound static constructor called twice - impossible");
            
            if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value == false)
            {
                var group = new ExclusiveGroup(GroupTag<G1>.bitmask | GroupTag<G2>.bitmask | GroupTag<G3>.bitmask);

                _Groups = new FasterList<ExclusiveGroupStruct>(1);
                _Groups.Add(group);

#if DEBUG
                var name = $"Compound: {typeof(G1).Name}-{typeof(G2).Name}-{typeof(G3).Name} ID {(uint)group.id}";
                GroupNamesMap.idToName[group] = name;
#endif
                //The hashname is independent from the actual group ID. this is fundamental because it is want
                //guarantees the hash to be the same across different machines
                GroupHashMap.RegisterGroup(group, typeof(GroupCompound<G1, G2, G3>).FullName);

                var exclusiveGroupStructs = _Groups.ToArrayFast(out var count);
                _GroupsHashSet = new HashSet<ExclusiveGroupStruct>(count);
                for (var index = 0; index < count; ++index)
                {
                    var exclusiveGroupStruct = exclusiveGroupStructs[index];
                    _GroupsHashSet.Add(exclusiveGroupStruct);
                }

                GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value = true;

                //all the combinations must share the same group and group hashset
                GroupCompound<G3, G1, G2>._Groups = _Groups;
                GroupCompound<G2, G3, G1>._Groups = _Groups;
                GroupCompound<G3, G2, G1>._Groups = _Groups;
                GroupCompound<G1, G3, G2>._Groups = _Groups;
                GroupCompound<G2, G1, G3>._Groups = _Groups;

                //all the constructor have been called now
                GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value = false;

                GroupCompound<G3, G1, G2>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G2, G3, G1>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G3, G2, G1>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G1, G3, G2>._GroupsHashSet = _GroupsHashSet;
                GroupCompound<G2, G1, G3>._GroupsHashSet = _GroupsHashSet;
                
                GroupCompound<G1, G2>.Add(group); //<G1/G2> and <G2/G1> must share the same array
                GroupCompound<G1, G3>.Add(group);
                GroupCompound<G2, G3>.Add(group);

                //This is done here to be sure that the group is added once per group tag
                //(if done inside the previous group compound it would be added multiple times)
                GroupTag<G1>.Add(group);
                GroupTag<G2>.Add(group);
                GroupTag<G3>.Add(group);
            }
        }

        public static FasterReadOnlyList<ExclusiveGroupStruct> Groups
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => new(_Groups);
        }

        public static ExclusiveBuildGroup BuildGroup
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => new(_Groups[0], 1);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool Includes(ExclusiveGroupStruct group)
        {
            DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed");

            return _GroupsHashSet.Contains(group);
        }

        internal static void Add(ExclusiveGroupStruct group)
        {
#if DEBUG && !PROFILE_SVELTO
            for (var i = 0; i < _Groups.count; ++i)
                if (_Groups[i] == group)
                    throw new System.Exception("this test must be transformed in unit test");
#endif

            _Groups.Add(group);
            _GroupsHashSet.Add(group);
        }

        static readonly FasterList<ExclusiveGroupStruct> _Groups;
        static readonly HashSet<ExclusiveGroupStruct> _GroupsHashSet;

        //we are changing this with Interlocked, so it cannot be readonly
        static int isInitializing;
    }

    public abstract class GroupCompound<G1, G2>: ITouchedByReflection
            where G1 : GroupTag<G1>
            where G2 : GroupTag<G2>
    {
        static GroupCompound()
        {
            /// c# Static constructors are guaranteed to be thread safe and not called more than once
            if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0)
                throw new Exception($"{typeof(GroupCompound<G1, G2>).FullName} GroupCompound static constructor called twice - impossible");
            
            if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value == false)
            {
                range = GroupTag<G1>.range > GroupTag<G2>.range ? GroupTag<G1>.range : GroupTag<G2>.range;

                var initialSize = range;
                _Groups = new FasterList<ExclusiveGroupStruct>(initialSize);

                var group = new ExclusiveGroup(initialSize, GroupTag<G1>.bitmask | GroupTag<G2>.bitmask);
                
#if DEBUG
                for (uint i = 0; i < initialSize; ++i)
                {
                    var groupID = group.id + i;
                    var name = $"Compound: {typeof(G1).Name}-{typeof(G2).Name} ID {groupID}";

                    GroupNamesMap.idToName[group + i] = name;
                }
#endif

                for (uint i = 0; i < initialSize; ++i)
                {
                    var exclusiveGroupStruct = group + i;
                    
                    //The hashname is independent from the actual group ID. this is fundamental because it is want
                    //guarantees the hash to be the same across different machines
                    GroupHashMap.RegisterGroup(exclusiveGroupStruct, typeof(GroupCompound<G1, G2>).FullName + i);
                    
                    _Groups.Add(exclusiveGroupStruct);
                    //every abstract group preemptively adds this group, it may or may not be empty in future
                    GroupTag<G1>.Add(exclusiveGroupStruct);
                    GroupTag<G2>.Add(exclusiveGroupStruct);
                }
                
                var exclusiveGroupStructs = _Groups.ToArrayFast(out var count);
                _GroupsHashSet = new HashSet<ExclusiveGroupStruct>(count);
                for (var index = 0; index < count; index++)
                {
                    var exclusiveGroupStruct = exclusiveGroupStructs[index];
                    _GroupsHashSet.Add(exclusiveGroupStruct);
                }

                GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value = true;
                GroupCompound<G2, G1>._Groups = _Groups;
                //all the constructor have been called now
                GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value = false;
                
                GroupCompound<G2, G1>.range = initialSize;
                GroupCompound<G2, G1>._GroupsHashSet = _GroupsHashSet;
            }
        }

        public static FasterReadOnlyList<ExclusiveGroupStruct> Groups
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => new(_Groups);
        }

        public static ExclusiveBuildGroup BuildGroup
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => new(_Groups[0], range);
        }

        //TODO there is an overlap between this method and ExclusiveGroupExtensions FoundIn
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool Includes(ExclusiveGroupStruct group)
        {
            DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed");

            return _GroupsHashSet.Contains(group);
        }

        internal static void Add(ExclusiveGroupStruct group)
        {
#if DEBUG && !PROFILE_SVELTO
            for (var i = 0; i < _Groups.count; ++i)
                if (_Groups[i] == group)
                    throw new System.Exception("this test must be transformed in unit test");
#endif

            _Groups.Add(group);
            _GroupsHashSet.Add(group);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static uint Offset(ExclusiveGroupStruct group)
        {
            return BuildGroup.Offset(group);
        }

        static readonly FasterList<ExclusiveGroupStruct> _Groups;
        static readonly HashSet<ExclusiveGroupStruct> _GroupsHashSet;

        static ushort range;

        static int isInitializing;
    }

    /// <summary>
    ///     A Group Tag holds initially just a group, itself. However the number of groups can grow with the number of
    ///     combinations of GroupTags including this one. This because a GroupTag is an adjective and different entities
    ///     can use the same adjective together with other ones. Albeit since I need to be able to iterate over all the
    ///     groups with the same adjective, a group tag needs to hold all the group compounds using it.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public abstract class GroupTag<T>: ITouchedByReflection where T : GroupTag<T>
    {
        static GroupTag()
        {
            if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0)
                throw new Exception("GroupTag static constructor called twice - impossible");
            
            //GroupTag can set values for ranges and bitmasks in their static constructors so they must be called first
            //there is no other way around this, as the base static constructor will be called once base fields are touched
            //RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);
            typeof(T).TypeInitializer?.Invoke(null, null); //must use this because if a specialised GroupTag is called first will call the this constructor before having the chance to initialise the protected values. This will force to initialise the values no matter what (and won't call the base constructor again because already executing)
            
            var initialRange = range; //range may be overriden by the constructor previously called

            if (initialRange == 0) //means never initialised by a inherited static constructor
            {
                initialRange = 1;
                range = 1;
            }

            var group = new ExclusiveGroup(initialRange, bitmask);
            for (uint i = 0; i < initialRange; ++i)
            {
                _Groups.Add(group + i);
                //The hashname is independent from the actual group ID. this is fundamental because it is want
                //guarantees the hash to be the same across different machines
                GroupHashMap.RegisterGroup(group + i, typeof(GroupTag<T>).FullName + i);
            }
#if DEBUG
            var typeInfo = typeof(T);
            for (uint i = 0; i < initialRange; ++i)
            {
                var groupID = group.id + i;
                var name = $"Compound: {typeInfo.Name} ID {groupID}";

                var typeInfoBaseType = typeInfo.BaseType;
                //todo: this should shield from using a pattern different than public class GROUP_NAME : GroupTag<GROUP_NAME> {} however I am not sure it's working
                if (typeInfoBaseType.GenericTypeArguments[0] != typeInfo)
                    throw new ECSException("Invalid Group Tag declared");

                GroupNamesMap.idToName[group + i] = name;
            }
#endif
            var exclusiveGroupStructs = _Groups.ToArrayFast(out var count);
            _GroupsHashSet = new HashSet<ExclusiveGroupStruct>(count);
            for (var index = 0; index < count; index++)
            {
                var exclusiveGroupStruct = exclusiveGroupStructs[index];
                _GroupsHashSet.Add(exclusiveGroupStruct);
            }
        }

        public static FasterReadOnlyList<ExclusiveGroupStruct> Groups
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => new(_Groups);
        }

        public static ExclusiveBuildGroup BuildGroup
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => new(_Groups[0], range);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool Includes(ExclusiveGroupStruct group)
        {
            DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed");

            return _GroupsHashSet.Contains(group);
        }

        //Each time a new combination of group tags is found a new group is added.
        internal static void Add(ExclusiveGroupStruct group)
        {
#if DEBUG && !PROFILE_SVELTO
            for (var i = 0; i < _Groups.count; ++i)
                if (_Groups[i] == group)
                    throw new System.Exception("this test must be transformed in unit test");
#endif

            _Groups.Add(group);
            _GroupsHashSet.Add(group);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static uint Offset(ExclusiveGroupStruct group)
        {
            return BuildGroup.Offset(group);
        }

        static readonly FasterList<ExclusiveGroupStruct> _Groups = new FasterList<ExclusiveGroupStruct>(1);
        static readonly HashSet<ExclusiveGroupStruct> _GroupsHashSet;

        //we are changing this with Interlocked, so it cannot be readonly
        static int isInitializing;

        //special group attributes, at the moment of writing this comment, only the disabled group has a special attribute

        //Allow to call GroupTag static constructors like
//                public class Dead: GroupTag<Dead>
//                {
//                    static Dead()
//                    {
//                        bitmask = ExclusiveGroupBitmask.DISABLED_BIT;
//                    }
//                };

        protected internal static ExclusiveGroupBitmask bitmask;
        //set a number different than 0 to create a range of groups instead of a single group
        //example of usage:
//        public class VehicleGroup:GroupTag<VehicleGroup>
//        {
//            static VehicleGroup()
//            {
//                range = (ushort)Data.MaxTeamCount;
//            }
//        }

        protected internal static ushort range;
    }
}